/*  
 * Copyright 2011, Asamm Software, s.r.o.
 * 
 * This file is part of LocusAddonPublicLib.
 * 
 * LocusAddonPublicLib is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *  
 * LocusAddonPublicLib is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *  
 * You should have received a copy of the GNU General Public License
 * along with LocusAddonPublicLib.  If not, see <http://www.gnu.org/licenses/>.
 */

package menion.android.locus.addon.publiclib.utils;

import java.util.ArrayList;

import android.database.AbstractCursor;
import android.database.CursorIndexOutOfBoundsException;
import android.database.CursorWindow;

/**
 * A mutable cursor implementation backed by an array of {@code Object}s. Use
 * {@link #newRow()} to add rows. Automatically expands internal capacity as
 * needed.
 */
public class DataCursor extends AbstractCursor {

	private final String[] columnNames;
	private Object[] data;
	private int rowCount = 0;
	private final int columnCount;

	/**
	 * Constructs a new cursor with the given initial capacity.
	 * 
	 * @param columnNames
	 *            names of the columns, the ordering of which determines column
	 *            ordering elsewhere in this cursor
	 * @param initialCapacity
	 *            in rows
	 */
	public DataCursor(String[] columnNames, int initialCapacity) {
		this.columnNames = columnNames;
		this.columnCount = columnNames.length;

		if (initialCapacity < 1) {
			initialCapacity = 1;
		}

		this.data = new Object[columnCount * initialCapacity];
	}

	/**
	 * Constructs a new cursor.
	 * 
	 * @param columnNames
	 *            names of the columns, the ordering of which determines column
	 *            ordering elsewhere in this cursor
	 */
	public DataCursor(String[] columnNames) {
		this(columnNames, 16);
	}

	/**
	 * Gets value at the given column for the current row.
	 */
	private Object get(int column) {
		if (column < 0 || column >= columnCount) {
			throw new CursorIndexOutOfBoundsException("Requested column: "
					+ column + ", # of columns: " + columnCount);
		}
		if (mPos < 0) {
			throw new CursorIndexOutOfBoundsException("Before first row.");
		}
		if (mPos >= rowCount) {
			throw new CursorIndexOutOfBoundsException("After last row.");
		}
		return data[mPos * columnCount + column];
	}

	/**
	 * Adds a new row to the end and returns a builder for that row. Not safe
	 * for concurrent use.
	 * 
	 * @return builder which can be used to set the column values for the new
	 *         row
	 */
	public RowBuilder newRow() {
		rowCount++;
		int endIndex = rowCount * columnCount;
		ensureCapacity(endIndex);
		int start = endIndex - columnCount;
		return new RowBuilder(start, endIndex);
	}

	/**
	 * Adds a new row to the end with the given column values. Not safe for
	 * concurrent use.
	 * 
	 * @throws IllegalArgumentException
	 *             if {@code columnValues.length !=
	 *  columnNames.length}
	 * @param columnValues
	 *            in the same order as the the column names specified at cursor
	 *            construction time
	 */
	public void addRow(Object[] columnValues) {
		if (columnValues.length != columnCount) {
			throw new IllegalArgumentException("columnNames.length = "
					+ columnCount + ", columnValues.length = "
					+ columnValues.length);
		}

		int start = rowCount++ * columnCount;
		ensureCapacity(start + columnCount);
		System.arraycopy(columnValues, 0, data, start, columnCount);
	}

	/**
	 * Adds a new row to the end with the given column values. Not safe for
	 * concurrent use.
	 * 
	 * @throws IllegalArgumentException
	 *             if {@code columnValues.size() !=
	 *  columnNames.length}
	 * @param columnValues
	 *            in the same order as the the column names specified at cursor
	 *            construction time
	 */
	public void addRow(Iterable<?> columnValues) {
		int start = rowCount * columnCount;
		int end = start + columnCount;
		ensureCapacity(end);

		if (columnValues instanceof ArrayList<?>) {
			addRow((ArrayList<?>) columnValues, start);
			return;
		}

		int current = start;
		Object[] localData = data;
		for (Object columnValue : columnValues) {
			if (current == end) {
				// TODO: null out row?
				throw new IllegalArgumentException(
						"columnValues.size() > columnNames.length");
			}
			localData[current++] = columnValue;
		}

		if (current != end) {
			// TODO: null out row?
			throw new IllegalArgumentException(
					"columnValues.size() < columnNames.length");
		}

		// Increase row count here in case we encounter an exception.
		rowCount++;
	}

	/** Optimization for {@link ArrayList}. */
	private void addRow(ArrayList<?> columnValues, int start) {
		int size = columnValues.size();
		if (size != columnCount) {
			throw new IllegalArgumentException("columnNames.length = "
					+ columnCount + ", columnValues.size() = " + size);
		}

		rowCount++;
		Object[] localData = data;
		for (int i = 0; i < size; i++) {
			localData[start + i] = columnValues.get(i);
		}
	}

	/** Ensures that this cursor has enough capacity. */
	private void ensureCapacity(int size) {
		if (size > data.length) {
			Object[] oldData = this.data;
			int newSize = data.length * 2;
			if (newSize < size) {
				newSize = size;
			}
			this.data = new Object[newSize];
			System.arraycopy(oldData, 0, this.data, 0, oldData.length);
		}
	}

	/**
	 * Builds a row, starting from the left-most column and adding one column
	 * value at a time. Follows the same ordering as the column names specified
	 * at cursor construction time.
	 */
	public class RowBuilder {

		private int index;
		private final int endIndex;

		RowBuilder(int index, int endIndex) {
			this.index = index;
			this.endIndex = endIndex;
		}

		/**
		 * Sets the next column value in this row.
		 * 
		 * @throws CursorIndexOutOfBoundsException
		 *             if you try to add too many values
		 * @return this builder to support chaining
		 */
		public RowBuilder add(Object columnValue) {
			if (index == endIndex) {
				throw new CursorIndexOutOfBoundsException(
						"No more columns left.");
			}

			data[index++] = columnValue;
			return this;
		}
	}

	// AbstractCursor implementation.

	@Override
	public int getCount() {
		return rowCount;
	}

	@Override
	public String[] getColumnNames() {
		return columnNames;
	}

	@Override
	public String getString(int column) {
		Object value = get(column);
		if (value == null)
			return null;
		return value.toString();
	}

	@Override
	public short getShort(int column) {
		Object value = get(column);
		if (value == null)
			return 0;
		if (value instanceof Number)
			return ((Number) value).shortValue();
		return Short.parseShort(value.toString());
	}

	@Override
	public int getInt(int column) {
		Object value = get(column);
		if (value == null)
			return 0;
		if (value instanceof Number)
			return ((Number) value).intValue();
		return Integer.parseInt(value.toString());
	}

	@Override
	public long getLong(int column) {
		Object value = get(column);
		if (value == null)
			return 0;
		if (value instanceof Number)
			return ((Number) value).longValue();
		return Long.parseLong(value.toString());
	}

	@Override
	public float getFloat(int column) {
		Object value = get(column);
		if (value == null)
			return 0.0f;
		if (value instanceof Number)
			return ((Number) value).floatValue();
		return Float.parseFloat(value.toString());
	}

	@Override
	public double getDouble(int column) {
		Object value = get(column);
		if (value == null)
			return 0.0d;
		if (value instanceof Number)
			return ((Number) value).doubleValue();
		return Double.parseDouble(value.toString());
	}

	@Override
	public byte[] getBlob(int column) {
		Object value = get(column);
		if (value == null)
			return new byte[0];
		if (value instanceof byte[])
			return ((byte[]) value);
		return new byte[0];
	}

	@Override
	public boolean isNull(int column) {
		return get(column) == null;
	}

	public void fillWindow(int position, CursorWindow window) {
		if (position < 0 || position >= getCount()) {
			return;
		}
		window.acquireReference();
		try {
			int oldpos = mPos;
			mPos = position - 1;
			window.clear();
			window.setStartPosition(position);
			int columnNum = getColumnCount();
			window.setNumColumns(columnNum);
			while (moveToNext() && window.allocRow()) {
				for (int i = 0; i < columnNum; i++) {
					byte[] field = getBlob(i);
					if (field != null) {
						if (!window.putBlob(field, mPos, i)) {
							window.freeLastRow();
							break;
						}
					} else {
						if (!window.putNull(mPos, i)) {
							window.freeLastRow();
							break;
						}
					}
				}
			}

			mPos = oldpos;
		} catch (IllegalStateException e) {
			// simply ignore it
		} finally {
			window.releaseReference();
		}
	}
}
